"use client"; import * as React from "react"; import { PaginationState, SortingState, ColumnFiltersState, GroupingState } from "@tanstack/react-table"; import { ClientVirtualTable } from "@/components/client-table-v2/client-virtual-table"; import { TestProduct } from "@/db/schema/test-table-v2"; import { productColumns, orderColumns } from "./columns"; import { OrderWithDetails } from "./column-defs"; import { getAllProducts, getProductTableData, getOrderTableData, getProductTableDataWithGrouping, GroupInfo, } from "./actions"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { ChevronDown, ChevronRight, Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; // ============================================================ // Reusable Loading Overlay Component // ============================================================ function LoadingOverlay({ isLoading, children }: { isLoading: boolean; children: React.ReactNode }) { return (
{children} {isLoading && (
Loading...
)}
); } // ============================================================ // Pattern 1: Client-Side Table // ============================================================ function ClientSideTable() { const [data, setData] = React.useState([]); const [isLoading, setIsLoading] = React.useState(true); React.useEffect(() => { const fetchData = async () => { setIsLoading(true); try { const products = await getAllProducts(); setData(products); } catch (error) { console.error("Failed to fetch products:", error); } finally { setIsLoading(false); } }; fetchData(); }, []); return (
Pattern 1: Client-Side fetchMode="client"
모든 데이터를 한 번에 받아와 클라이언트에서 필터링/정렬/페이지네이션/그룹핑 처리합니다.
적합: 데이터 1000건 이하, 빠른 인터랙션 필요 시
✅ 그룹핑: 헤더 우클릭 → Group by [Column]
); } // ============================================================ // Pattern 2: Factory Service (Server-Side) // ============================================================ function FactoryServiceTable() { const [data, setData] = React.useState([]); const [totalRows, setTotalRows] = React.useState(0); const [isLoading, setIsLoading] = React.useState(true); // Table state const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 10, }); const [sorting, setSorting] = React.useState([]); const [columnFilters, setColumnFilters] = React.useState([]); const [globalFilter, setGlobalFilter] = React.useState(""); // Fetch data on state change React.useEffect(() => { const fetchData = async () => { setIsLoading(true); try { const result = await getProductTableData({ pagination, sorting, columnFilters, globalFilter, }); setData(result.data); setTotalRows(result.totalRows); } catch (error) { console.error("Failed to fetch products:", error); } finally { setIsLoading(false); } }; fetchData(); }, [pagination, sorting, columnFilters, globalFilter]); return (
Pattern 2: Factory Service fetchMode="server" createTableService
createTableService로 서버 액션을 자동 생성합니다.
적합: 단순 CRUD, 마스터 테이블 조회
⚠️ 그룹핑: 서버 모드에서는 별도 구현 필요 (Pattern 2-B 참고)
); } // ============================================================ // Pattern 2-B: Server-Side Grouping (Context Menu 방식) // ============================================================ function ServerGroupingTable() { const [grouping, setGrouping] = React.useState([]); const [expandedGroups, setExpandedGroups] = React.useState([]); const [groups, setGroups] = React.useState([]); const [flatData, setFlatData] = React.useState([]); const [isGrouped, setIsGrouped] = React.useState(false); const [isLoading, setIsLoading] = React.useState(true); const [totalRows, setTotalRows] = React.useState(0); const [sorting, setSorting] = React.useState([]); const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 10, }); // 데이터 페칭 React.useEffect(() => { const fetchData = async () => { setIsLoading(true); try { const result = await getProductTableDataWithGrouping( { pagination, grouping, sorting }, expandedGroups ); if ('groups' in result) { setGroups(result.groups); setIsGrouped(true); setFlatData([]); } else { setFlatData(result.data); setTotalRows(result.totalRows); setIsGrouped(false); setGroups([]); } } catch (error) { console.error("Failed to fetch:", error); } finally { setIsLoading(false); } }; fetchData(); }, [pagination, grouping, sorting, expandedGroups]); // 그룹 토글 const toggleGroup = (groupKey: string) => { setExpandedGroups(prev => prev.includes(groupKey) ? prev.filter(k => k !== groupKey) : [...prev, groupKey] ); }; // 그룹핑 상태 변경 핸들러 (Context Menu에서 호출됨) const handleGroupingChange = React.useCallback((updater: GroupingState | ((old: GroupingState) => GroupingState)) => { const newGrouping = typeof updater === 'function' ? updater(grouping) : updater; setGrouping(newGrouping); setExpandedGroups([]); // 그룹핑 변경 시 확장 상태 초기화 }, [grouping]); return (
Pattern 2-B: Server-Side Grouping fetchMode="server" GROUP BY
서버에서 GROUP BY + 집계 쿼리로 그룹 정보를 조회합니다.
✅ 그룹핑: 헤더 우클릭 → Group by [Column] (category, status, isNew만 지원)
{/* 현재 그룹핑 상태 표시 */} {grouping.length > 0 && (
Grouped by: {grouping.map((col) => ( {col} ))}
)} {/* Content with Loading Overlay */}
{isGrouped ? ( // Grouped View - Custom Rendering
{groups.length === 0 ? (
No data
) : ( groups.map((group) => (
{/* Group Header */} {/* Expanded Rows */} {expandedGroups.includes(group.groupKey) && group.rows && (
{group.rows.map((row) => ( ))}
ID SKU Name Price Stock
{row.id} {row.sku} {row.name} {new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(parseFloat(row.price))} {row.stock}
)}
)) )}
) : ( // Normal Table View with Context Menu Grouping )}
); } // ============================================================ // Pattern 3: Custom Service (Server-Side with Joins) // ============================================================ function CustomServiceTable() { const [data, setData] = React.useState([]); const [totalRows, setTotalRows] = React.useState(0); const [isLoading, setIsLoading] = React.useState(true); // Table state const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 10, }); const [sorting, setSorting] = React.useState([]); const [columnFilters, setColumnFilters] = React.useState([]); const [globalFilter, setGlobalFilter] = React.useState(""); // Fetch data on state change React.useEffect(() => { const fetchData = async () => { setIsLoading(true); try { const result = await getOrderTableData({ pagination, sorting, columnFilters, globalFilter, }); setData(result.data); setTotalRows(result.totalRows); } catch (error) { console.error("Failed to fetch orders:", error); } finally { setIsLoading(false); } }; fetchData(); }, [pagination, sorting, columnFilters, globalFilter]); return (
Pattern 3: Custom Service fetchMode="server" DrizzleTableAdapter
DrizzleTableAdapter를 도구로 사용하여 복잡한 조인 쿼리를 직접 작성합니다.
적합: 여러 테이블 조인, 복잡한 비즈니스 로직
⚠️ 그룹핑: 가상 컬럼(조인 결과)은 서버 GROUP BY 불가
); } // ============================================================ // Main Page // ============================================================ export default function TableV2TestPage() { return (

ClientVirtualTable V2 - 데이터 페칭 패턴 테스트

GUIDE.md에 정의된 데이터 페칭 패턴과 그룹핑 처리 방법을 테스트합니다.
테스트 전 시딩이 필요합니다: npx tsx db/seeds/test-table-v2.ts

1. Client-Side 2. Factory Service 2-B. Server Grouping 3. Custom Service {/* Summary Table */} 패턴별 그룹핑 지원 현황
패턴 그룹핑 방식 가상 컬럼 지원 비고
1. Client-Side TanStack Grouping ✓ 지원 메모리에서 처리, 전체 데이터 필요
2. Factory Service 미지원 - 별도 구현 필요 (2-B 참고)
2-B. Server Grouping DB GROUP BY ✗ 불가 serverGroupable 컬럼만 가능
3. Custom Service 커스텀 구현 선택적 쿼리 설계에 따라 다름
{/* Column Groupability Info */} 컬럼별 서버 그룹핑 지원 여부 meta.serverGroupable 플래그로 DB GROUP BY 가능 여부를 표시합니다.
헤더 우클릭 시 "Group by [Column]" 메뉴가 표시됩니다.
{productColumns.map((col) => { if (!('accessorKey' in col)) return null; const meta = col.meta as { serverGroupable?: boolean } | undefined; const isGroupable = meta?.serverGroupable; return ( {col.accessorKey as string} {isGroupable && " ✓"} ); })}
); }